本文章同時發佈於:
最近看反正我很閒有點中毒,我拿他們的影片來比喻JWT驗證的各種情境劇好了ᕕ(ᐛ)ᕗ:
小鐘:一級警報,一級警報。
小樂:什麼一級警報。
小鐘:就Auth-Service發出去的token,為了要讓另一台電腦的Other-Service能夠驗證,所以我把secret key
給Other-Service,現在那台電腦被駭了,secret key
就被偷了。
小樂:蛤?
小鐘:我們Auth-Service的secret key
被偷了。
小樂:被偷了?欸不是,secret key
這麼重要,你給Other-Service幹嘛,你為什麼不在Auth-Service上面設計一個驗證token的API不就好了不是嗎,你現在給我被偷是怎樣。
小鐘:之前本來要設計,但你也知道Other-Service每次要驗證都要呼叫Auth-Service的驗證API就很慢嘛。啊Other-Service現在隨便就幾千個偽造token在呼叫,我們secret key
真的被偷了。
小樂:不是啊,Other-Service驗證完的結果可以放快取啊,這樣除了第一次驗證要呼叫驗證API,之後直到token過期前都讀快取就好了啊。不是,這種事情你怎麼不早講。
小鐘:我就
我們目前較常看到的方式都是HMAC的作法,即是:
透過
secret key
與使用者資訊做雜湊運算
仔細來說就是,Auth-Service將此token使用哪種演算法與使用者資訊透過Base64編碼成「紅色與紫色的字串」,並使用secret key
來與紅色與紫色的字串進行雜湊運算得到「藍色的字串」後,組成token。
程式範例如下,也可點我線上運行:
const jwt = require('jsonwebtoken');
const token = jwt.sign({ user: 'York' }, "IAmSecretKey");
JWT發放出去後,就會有驗證JWT的需求,而驗證方法就是:
驗證方一樣使用secret key
來與「紅色與紫色的字串」進行雜湊運算,並將得到的字串與「token的藍色字串」進行比對。程式碼如下,也可點我線上運行:
const jwt = require('jsonwebtoken')
const decoded = jwt.verify(token, 'IAmSecretKey')
console.log(decoded.user)
而小鐘與小樂的討論提供了兩種驗證做法為:
secret key
而這兩種方法都有以下要注意的地方:
secret key
,如果此單位是在我們的「可管控的」,比如說:都在同個機器上的Server,就比較沒有安全問題。但如果是跨機器或者第三方這些「較難管控甚至是不可管控」的單位,要分享此key就成了大問題,因為只要此key一流出,Auth-Service所發的token就會被盜用。secret key
外流的問題,但每次的驗證都需要呼叫一次API,會很浪費時間,所以必須把驗證結果存到快取中,以避免一直無謂的呼叫。方法二是我目前比較常看到的方法,如果各位有知道不同的方法也歡迎分享討論,不過,這種方法就需要再動到快取的技術,是否有更簡單的方法?那你也許可以考慮RS256。
前面所提的HMAC,是被稱為HS256
的演算法,做法是:
用「一把」私密的鑰匙來做雜湊運算生產token
而RS256
則是:
用「兩把」鑰匙分別是公鑰與私鑰以非對稱的方法來簽證token。
以Auth-Service來說,Auth-Service透過私鑰簽證token,驗證方以公鑰來驗證token,如此以來,發放公鑰給「較難管控甚至是不可管控」的單位時,我們不必擔心安全性,因為Auth-Service用來簽證的私鑰從沒外流過。
使用RS256的算法程式碼如下,也可點我線上運行:
const jwt = require('jsonwebtoken')
const privateKey=`-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgHskuZKKc7NL447r40FHHyX3lv8Cf5KybCauK8SRUswnuI3F+e0C
bwtMfjkg0j7wQDH2HCCjEsiwTjXJd9QxWcxb38gdLrpHRftdWaeWYs41aoWJGiBk
DBHjKLqLGzmYhQaGl37XzNUqab/32DdsI1Fme7o9ANEwUPxsEWQvsMMbAgMBAAEC
gYApGWO6Le1ZrP1g6Qeq9MLHmC/UIpBTdKs16bF/5IS+0I7++lFksgg/vCLwjCy/
hs3WHu7aUbLmOjmQKBKPRn1ShdtEKuM5K1pCd7Anj4YLsQjGTRJONNgKw5U9nQiw
YYbvghERLOVPhfab3IPfhYZW7Ye4KmjBjKjU/5zkxHdn+QJBAOYpfSj2hW187atD
I34Fq7ee8DTCElHpkkemgKsPPelG5mYicbSZXePkrp/RPr8DTAVVgm7iBsZP9YLp
h9R0UIUCQQCI966ubKmsLo1T3TLupNeY1mrPl0a9UEDy8tzEaQlFMI9rXgnfXv/n
ZoLG4NPu2CFUemJt6jeVXNMsmFHBF+cfAkBmWwMPKXqy80DazfPFwo3YDfWy8K+m
/+GOvaww5olY6a/iseSxNRc9FuDVr/9ggP3YzWtBFoF+xeZf/qzqPYPlAkEAiBeC
K7GwjXLb3j5lgxWrWyOBka7ADQ8W2c9SaJ3tJiBwAMC5koa0Qtpqiu2N5z49L9FC
x+/3NqO6+A6I/RGhBQJBAK1oJCuv9sl1EWRoLOpr3THcIV3xL3jHyckt7EpNBaTT
Upkj9+K/+wNwjNXvlPvYRjuLn5M83NGsuBCWL+h+ZL8=
-----END RSA PRIVATE KEY-----`
const token = jwt.sign({ user: 'York' }, privateKey, { algorithm: 'RS256' });
而驗證的話我們就要用公鑰來驗證,也可點我線上運行:
const cert = `-----BEGIN PUBLIC KEY-----
MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHskuZKKc7NL447r40FHHyX3lv8C
f5KybCauK8SRUswnuI3F+e0CbwtMfjkg0j7wQDH2HCCjEsiwTjXJd9QxWcxb38gd
LrpHRftdWaeWYs41aoWJGiBkDBHjKLqLGzmYhQaGl37XzNUqab/32DdsI1Fme7o9
ANEwUPxsEWQvsMMbAgMBAAE=
-----END PUBLIC KEY-----`
const decoded = jwt.verify(token, cert)
console.log(decoded.user)
RS256用了很簡單的方法解決了不同單位的驗證需求,但為什麼比較少聽到呢?以下是我的兩種猜測:
雖然是猜測,但對於第一點我們還是可以驗證看看,我們以Benchmark.js
來效能測試此兩種方法,也可點我線上運行:
const Benchmark = require('benchmark')
const jwt = require('jsonwebtoken')
const suite = new Benchmark.Suite
suite
.add('HS256', function() {
const token = jwt.sign({ user: 'York' }, "IAmSecretKey")
const decoded = jwt.verify(token, 'IAmSecretKey')
})
.add('RS256', function() {
const privateKey=`-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgHskuZKKc7NL447r40FHHyX3lv8Cf5KybCauK8SRUswnuI3F+e0C
bwtMfjkg0j7wQDH2HCCjEsiwTjXJd9QxWcxb38gdLrpHRftdWaeWYs41aoWJGiBk
DBHjKLqLGzmYhQaGl37XzNUqab/32DdsI1Fme7o9ANEwUPxsEWQvsMMbAgMBAAEC
gYApGWO6Le1ZrP1g6Qeq9MLHmC/UIpBTdKs16bF/5IS+0I7++lFksgg/vCLwjCy/
hs3WHu7aUbLmOjmQKBKPRn1ShdtEKuM5K1pCd7Anj4YLsQjGTRJONNgKw5U9nQiw
YYbvghERLOVPhfab3IPfhYZW7Ye4KmjBjKjU/5zkxHdn+QJBAOYpfSj2hW187atD
I34Fq7ee8DTCElHpkkemgKsPPelG5mYicbSZXePkrp/RPr8DTAVVgm7iBsZP9YLp
h9R0UIUCQQCI966ubKmsLo1T3TLupNeY1mrPl0a9UEDy8tzEaQlFMI9rXgnfXv/n
ZoLG4NPu2CFUemJt6jeVXNMsmFHBF+cfAkBmWwMPKXqy80DazfPFwo3YDfWy8K+m
/+GOvaww5olY6a/iseSxNRc9FuDVr/9ggP3YzWtBFoF+xeZf/qzqPYPlAkEAiBeC
K7GwjXLb3j5lgxWrWyOBka7ADQ8W2c9SaJ3tJiBwAMC5koa0Qtpqiu2N5z49L9FC
x+/3NqO6+A6I/RGhBQJBAK1oJCuv9sl1EWRoLOpr3THcIV3xL3jHyckt7EpNBaTT
Upkj9+K/+wNwjNXvlPvYRjuLn5M83NGsuBCWL+h+ZL8=
-----END RSA PRIVATE KEY-----`
const token = jwt.sign({ user: 'York' }, privateKey, { algorithm: 'RS256' });
const cert = `-----BEGIN PUBLIC KEY-----
MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHskuZKKc7NL447r40FHHyX3lv8C
f5KybCauK8SRUswnuI3F+e0CbwtMfjkg0j7wQDH2HCCjEsiwTjXJd9QxWcxb38gd
LrpHRftdWaeWYs41aoWJGiBkDBHjKLqLGzmYhQaGl37XzNUqab/32DdsI1Fme7o9
ANEwUPxsEWQvsMMbAgMBAAE=
-----END PUBLIC KEY-----`
const decoded = jwt.verify(token, cert)
})
.on('cycle', function(event) {
console.log(String(event.target));
})
.on('complete', function() {
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
.run({ 'async': true });
可以看到結果如下,HS256比RS256快了約17倍,
HS256可以一秒執行3萬多次,而RS256一秒執行2千多次,雖然HS256快了不少,但雙方都執行相當快是否需要考慮速度就是一個問題了。
另外,而且如果真的要考慮速度,HS256較常看到的作法是
第一次呼叫Auth-Service的驗證API,並且結果存快取,之後再讀取快取的驗證結果
這個呼叫API的步驟,肯定是比純運算來得久非常多,所以HS256的實作上又不一定比RS256快了,並且RS256的結果也是能存快取的,那麼RS256因為都不用呼叫驗證API,所以又反過來比HS256快了(ノ゚▽゚)ノ。
在比較上面,國外也有人詢問了此問題,Val_Melamed認為RS256很安全,但為什麼大家都常用HS256呢?
而Auth0(專門在做身份認證與授權的廠商)的員工是這樣回答的:
就是:「沒有啦,我們現在是用RS256來當預設的token演算法呀(´∀`)」
所以說,RS256還是提供了一個不錯的方法來驗證JWT,是可以考慮的,而較傳統的HS256也沒有錯,就取決於團隊想要用哪種作法。
而你會用哪種作法呢?歡迎分享與討論供大家參考~
這是我當初對JWT的一個誤解,那時我一直以為token是全部的字串都是加密的,事實上只有最後的藍色字串是有被雜湊演算或者非對稱簽證的,紅色與紫色的字串是Base64編碼並不是加密,他只是將資訊轉換成Base64的格式,他是可以直接轉回來的
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiWW9yayJ9.rgmKsHCtvBZRjOCjULB8aMyMtqP8VkWiz3__WO_QXnY
大家可以複製前兩段字串在此網站嘗試解碼,會得到以下結果:
謝謝你的閱讀,也歡迎分享討論~